package ru.amse.stroganova.ui.visual;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Stroke;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import ru.amse.stroganova.presentation.EdgePresentation;
import ru.amse.stroganova.presentation.GraphPresentation;
import ru.amse.stroganova.presentation.VertexPresentation;

/**
 * This class represents a tool for selecting, unselecting, moving etc. graph presentation elements.
 * 
 * 
 * @author Dasha Stroganova
 *
 */
public class GraphElementSelection {

	private GraphPresentation graphPresentation;

	private final Set<VertexPresentation> selectedVertices;

	private final Set<EdgePresentation> selectedEdges;

	private final List<VisualStateListener> listeners;
	
	private static final int EDGE_SELECTION_RADIX = 3;
	
	private static final int VERTEX_SELECTION_DISTANCE = 3;
	
	private static final Color SELCTION_COLOR = new Color(194, 12, 16);
	
	/**
	 * Creates new selection for given presentation.
	 * 
	 * @param graphPresentation
	 */
	public GraphElementSelection(GraphPresentation graphPresentation) {
		this.graphPresentation = graphPresentation;
		selectedVertices = new HashSet<VertexPresentation>();
		selectedEdges = new HashSet<EdgePresentation>();
		listeners = new ArrayList<VisualStateListener>();
	}
	
	/**
	 * Adds the specified VisualStateListaner to recive notification about state
	 * change.
	 * 
	 * @param listener
	 */
	public void addVisualStateListener(VisualStateListener listener) {
		listeners.add(listener);
	}
	
	/**
	 * Removes the specified VisualStateListaner from this presentation.
	 * 
	 * @param listener
	 */
	public void removeVisualStateListener(VisualStateListener listener) {
		listeners.remove(listener);
	}
	
	/**
	 * Sets new graph presentation.
	 * 
	 * @param graphPresentation
	 */
	public void setGraphPresentation(GraphPresentation graphPresentation) {
		this.graphPresentation = graphPresentation;
		unselect();
	}
	
	private void fireVisualStateChange() {
		for (VisualStateListener listener : listeners) {
			listener.visualStateChanged();
		}
	}
	
	private void fireVisualChandesCancelled() {
		for (VisualStateListener listener : listeners) {
			listener.visualChangesCancelled();
		}
	}
	
	/**
	 * Returns currently selected vertices.
	 * 
	 * @return currently selected vertices
	 */
	public Set<VertexPresentation> getSelectedVertices() {
		return selectedVertices;
	}
	
	/**
	 * Returns currently selected edges.
	 * 
	 * @return currently selected edges
	 */
	public Set<EdgePresentation> getSelectedEdges() {
		return selectedEdges;
	}
	
	/**
	 * Selects givet vertex presentation.
	 * 
	 * @param vp vertex presentation
	 */
	public void selectVertex(VertexPresentation vp) {
		selectedVertices.add(vp);
		//vp.select();
		fireVisualStateChange();
	}
	
	/**
	 * Selects givet edge presentation.
	 * 
	 * @param ep edge presentation
	 */
	public void selectEdge(EdgePresentation ep) {
		selectedEdges.add(ep);
		fireVisualStateChange();
	}

	/**
	 * Finds and marks element containing given point.
	 * 
	 * @param point
	 *            point
	 * 
	 * @return true if element was found; false otherwise
	 */
	public boolean select(Point point) {
		return selectVerticesOnPoint(point) || selectEdgesOnPoint(point);
	}

	/**
	 * Selects all item within given rectangle.
	 * 
	 * @param x1
	 *            left upper corner x
	 * @param y1
	 *            left upper corner y
	 * @param x2
	 *            right lower corner x
	 * @param y2
	 *            right lower corner y
	 */
	public void selectInRect(int x1, int y1, int x2, int y2) {
		for (VertexPresentation vp : graphPresentation.getVertexPresentations()) {
			if (vp.isInRect(x1, y1, x2, y2)) {
				selectedVertices.add(vp);
			}
		}
		for (EdgePresentation ep : graphPresentation.getEdgePresentations()) {
			if (ep.isInRect(x1, y1, x2, y2)) {
				selectedEdges.add(ep);
			}
		}
		if ((selectedEdges.size() != 0) || (selectedVertices.size() != 0)) {	
			fireVisualStateChange();
		}
	}

	/**
	 * Checkes whether given point lies whithin an already selected item.
	 * 
	 * @param point
	 *            point
	 * @return true if it does; false otherwise
	 */
	public boolean isInSelectedItem(Point point) {
		for (VertexPresentation vp : selectedVertices) {
			if (vp.containsPoint(point)) {
				return true;
			}
		}
		for (EdgePresentation ep : selectedEdges) {
			if (ep.containsPoint(point)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Unselects previously selected elements.
	 * 
	 */
	public void unselect() {
		selectedEdges.clear();
		selectedVertices.clear();
		fireVisualChandesCancelled();
	}

	/**
	 * Unselects item on given point
	 * 
	 * @param point
	 *            point
	 */
	public void unselectItem(Point point) {
		boolean isVertex = false;
		for (Iterator<VertexPresentation> it = selectedVertices.iterator(); it
				.hasNext();) {
			VertexPresentation vertex = it.next();
			if (vertex.containsPoint(point)) {
				isVertex = true;
				it.remove();
			}
		}
		if (!isVertex) {
			for (Iterator<EdgePresentation> it = selectedEdges.iterator(); it
					.hasNext();) {
				EdgePresentation edge = it.next();
				if (edge.containsPoint(point)) {
					it.remove();
				}
			}
		}
		if ((selectedEdges.size() == 0) && (selectedVertices.size() == 0)) {
			fireVisualChandesCancelled();
		} else {
			fireVisualStateChange();
		}
	}
	
	/**
	 * Moves selected vertex to the given point.
	 * 
	 * @param dx
	 *            horisontal shift
	 * @param dy
	 *            vertical shift
	 */
	public void moveSelectedVertex(int dx, int dy) {// Point point) {
		for (VertexPresentation vp : selectedVertices) {
			Point center = vp.getCenter();
			center.setLocation(center.x + dx, center.y + dy);
			if (center.x < VertexPresentation.RADIX
					|| center.y < VertexPresentation.RADIX) {
				return;
			}
		}
		for (VertexPresentation vp : selectedVertices) {
			Point center = vp.getCenter();
			center.setLocation(center.x + dx, center.y + dy);
			vp.setCenter(center);
		}
	}
	
	/**
	 * If graph is weighted and weight of some edge contains selected point,
	 * returns this edge presentation.
	 * 
	 * @param point
	 *            point
	 * @return edge presentation if it was found; null otherwise
	 */
	public EdgePresentation getEdgeWithSelectedWeight(Point point) {
		if (!graphPresentation.isWeighted()) {
			return null;
		}
		for (EdgePresentation ep : graphPresentation.getEdgePresentations()) {
			if (ep.isWeightSelected(point)) {
				return ep;
			}
		}
		return null;
	}

	/**
	 * If any selected edge end mark contains given point returns this edge.
	 * 
	 * @param point
	 * @return edge whose mark is selected; null if there is no such edge
	 */
	public EdgePresentation getEdgeWithSelectedEnd(Point point) {
		for (EdgePresentation ep : selectedEdges) {
			if (isEdgeEndSelected(ep, point) || isEdgeStartSelected(ep, point)) {
				return ep;
			}
		}
		return null;
	}
	
	/**
	 * Checkes whether given point lies whithin a vertex and returns this vertex.
	 * 
	 * @param center
	 *            center
	 * @return vertex presentation; null if the point is outside vertices
	 */
	//TODO Small duplicating. think.
	public VertexPresentation getVertex(Point center) {
		for (VertexPresentation vp : graphPresentation.getVertexPresentations()) {
			if (vp.containsPoint(center)) {
				return vp;
			}
		}
		return null;
	}
	
	private boolean selectVerticesOnPoint(Point point) {
		boolean result = false;
		VertexPresentation selectedVP = null;
		for (VertexPresentation vp : graphPresentation.getVertexPresentations()) {
			if (vp.containsPoint(point)) {
				selectedVP = vp;
				result = true;
			}
		}
		if (result) {
			selectedVertices.add(selectedVP);
			fireVisualStateChange();
		}
		return result;
	}
	
	private boolean selectEdgesOnPoint(Point point) {
		boolean result = false;
		EdgePresentation selectedEP = null;
		for (EdgePresentation ep : graphPresentation.getEdgePresentations()) {
			if (ep.containsPoint(point)) {
				selectedEP = ep;
				result = true;
			}
		}
		if (result) {
			selectedEdges.add(selectedEP);
			fireVisualStateChange();
		}
		return result;
	}

	/**
	 * Veryfies whether edge end mark was selected.
	 * 
	 * @param ep
	 * @param point
	 * @return true if it was; false otherwise
	 */
	public boolean isEdgeEndSelected(EdgePresentation ep, Point point) {
		return (square(ep.getEnd().x - point.x) + square(ep.getEnd().y - point.y) < square(EDGE_SELECTION_RADIX));
	}
	
	/**
	 * Veryfies whether edge start mark was selected.
	 * 
	 * @param ep
	 * @param point
	 * @return true if it was; false otherwise
	 */
	public boolean isEdgeStartSelected(EdgePresentation ep, Point point) {
		return (square(ep.getStart().x - point.x) + square(ep.getStart().y - point.y) < square(EDGE_SELECTION_RADIX));
	}
	
	private int square(int x) {
		return x * x;
	}
	
	/**
	 * Performs all painting required for selecting elements.
	 * 
	 * @param gr graphics
	 */
	//TODO may be this painting should be in separate class.
	public void paint(Graphics gr) {
		for (VertexPresentation vp : selectedVertices) {
			paintSelectedVertex(gr, vp);
		}
		for (EdgePresentation ep : selectedEdges) {
			paintSelectedEdge(gr, ep);
		}
	}
	
	private void paintSelectedVertex(Graphics gr, VertexPresentation vp) {
		Graphics2D gr2D = (Graphics2D) gr;
		float dash[] = {4.0f};
		BasicStroke dashed = new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f, dash, 0.0f);
		Stroke prevStroke = gr2D.getStroke();
		gr2D.setStroke(dashed);
		
		Point center = vp.getCenter();
		gr.setColor(SELCTION_COLOR);
		gr2D.draw(new RoundRectangle2D.Double(center.x - VertexPresentation.RADIX - VERTEX_SELECTION_DISTANCE, center.y - VertexPresentation.RADIX - VERTEX_SELECTION_DISTANCE,
					2 * (VertexPresentation.RADIX + VERTEX_SELECTION_DISTANCE), 2 * (VertexPresentation.RADIX + VERTEX_SELECTION_DISTANCE), 
					3 * VERTEX_SELECTION_DISTANCE, 3 * VERTEX_SELECTION_DISTANCE));
		
		gr2D.setStroke(prevStroke);
	}
	
	private void paintSelectedEdge(Graphics gr, EdgePresentation ep) {
		Point start = ep.getStart();
		Point end = ep.getEnd();
		
		gr.setColor(Color.BLACK);
		gr.drawOval(start.x - EDGE_SELECTION_RADIX, start.y - EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX);
		gr.drawOval(end.x - EDGE_SELECTION_RADIX, end.y - EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX);
		
		gr.setColor(SELCTION_COLOR);
		gr.fillOval(start.x - EDGE_SELECTION_RADIX, start.y - EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX);
		gr.fillOval(end.x - EDGE_SELECTION_RADIX, end.y - EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX, 2 * EDGE_SELECTION_RADIX);

	}
}
